
import os,re,sys,cgi,mimetypes,traceback
from cgi import parse_qs
from google.appengine.ext.webapp import util
from google.appengine.ext.webapp import template

def render(t='index.html',v={}):
  return template.render(
    os.path.join( os.path.dirname(__file__),'templates',t ),v
    )
  

REQUEST_MAPPINGS = {'GET':[],'POST':[],'PUT':[],'DELETE':[],}
ERROR_HANDLERS   = {}
MEDIA_ROOT       = os.path.join(os.path.dirname(__file__), 'media')
HTTP_MAPPINGS    = {
    100: 'CONTINUE',
    101: 'SWITCHING PROTOCOLS',
    200: 'OK',
    201: 'CREATED',
    202: 'ACCEPTED',
    203: 'NON-AUTHORITATIVE INFORMATION',
    204: 'NO CONTENT',
    205: 'RESET CONTENT',
    206: 'PARTIAL CONTENT',
    300: 'MULTIPLE CHOICES',
    301: 'MOVED PERMANENTLY',
    302: 'FOUND',
    303: 'SEE OTHER',
    304: 'NOT MODIFIED',
    305: 'USE PROXY',
    306: 'RESERVED',
    307: 'TEMPORARY REDIRECT',
    400: 'BAD REQUEST',
    401: 'UNAUTHORIZED',
    402: 'PAYMENT REQUIRED',
    403: 'FORBIDDEN',
    404: 'NOT FOUND',
    405: 'METHOD NOT ALLOWED',
    406: 'NOT ACCEPTABLE',
    407: 'PROXY AUTHENTICATION REQUIRED',
    408: 'REQUEST TIMEOUT',
    409: 'CONFLICT',
    410: 'GONE',
    411: 'LENGTH REQUIRED',
    412: 'PRECONDITION FAILED',
    413: 'REQUEST ENTITY TOO LARGE',
    414: 'REQUEST-URI TOO LONG',
    415: 'UNSUPPORTED MEDIA TYPE',
    416: 'REQUESTED RANGE NOT SATISFIABLE',
    417: 'EXPECTATION FAILED',
    500: 'INTERNAL SERVER ERROR',
    501: 'NOT IMPLEMENTED',
    502: 'BAD GATEWAY',
    503: 'SERVICE UNAVAILABLE',
    504: 'GATEWAY TIMEOUT',
    505: 'HTTP VERSION NOT SUPPORTED',
}


class RequestError(Exception):
    """A base exception for HTTP errors to inherit from."""
    status = 404
    
    def __init__(self, message, hide_traceback=False):
        super(RequestError, self).__init__(message)
        self.hide_traceback = hide_traceback

class Forbidden(RequestError):
    status = 403

class NotFound(RequestError):
    status = 404
    
    def __init__(self, message, hide_traceback=True):
        super(NotFound, self).__init__(message)
        self.hide_traceback = hide_traceback

class AppError(RequestError):
    status = 500

class Redirect(RequestError):
    """
    Redirects the user to a different URL.
    
    Slightly different than the other HTTP errors, the Redirect is less
    'OMG Error Occurred' and more 'let's do something exceptional'. When you
    redirect, you break out of normal processing anyhow, so it's a very similar
    case."""
    status = 302
    url = ''
    
    def __init__(self, url):
        self.url = url
        self.args = ["Redirecting to '%s'..." % self.url]


class Request(object):
    """An object to wrap the environ bits in a friendlier way."""
    GET = {}
    POST = {}
    PUT = {}
    
    def __init__(self, environ, start_response):
        self._environ = environ
        self._start_response = start_response
        self.setup_self()
    
    def setup_self(self):
        self.path = add_slash(self._environ.get('PATH_INFO', ''))
        self.method = self._environ.get('REQUEST_METHOD', 'GET').upper()
        self.query = self._environ.get('QUERY_STRING', '')
        self.content_length = 0
        
        try:
            self.content_length = int(self._environ.get('CONTENT_LENGTH', '0'))
        except ValueError:
            pass
        
        self.GET = self.build_get_dict()
        
        if self.method == 'POST':
            self.POST = self.build_complex_dict()
        elif self.method == 'PUT':
            self.PUT = self.build_complex_dict()
    
    
    def build_get_dict(self):
        """Takes GET data and rips it apart into a dict."""
        raw_query_dict = parse_qs(self._environ['QUERY_STRING'], keep_blank_values=1)
        query_dict = {}
        
        for key, value in raw_query_dict.items():
            if len(value) <= 1:
                query_dict[key] = value[0]
            else:
                # Since it's a list of multiple items, we must have seen more than
                # one item of the same name come in. Store all of them.
                query_dict[key] = value
        
        return query_dict
    
    
    def build_complex_dict(self):
        """Takes POST/PUT data and rips it apart into a dict."""
        raw_data = cgi.FieldStorage(fp=self._environ['wsgi.input'], environ=self._environ)
        query_dict = {}
        
        for field in raw_data:
            if isinstance(raw_data[field], list):
                # Since it's a list of multiple items, we must have seen more than
                # one item of the same name come in. Store all of them.
                query_dict[field] = [fs.value for fs in raw_data[field]]
            elif raw_data[field].filename:
                # We've got a file.
                query_dict[field] = raw_data[field]
            else:
                query_dict[field] = raw_data[field].value
        
        return query_dict


class Response(object):
    headers = []
    
    def __init__(self, output, headers=None, status=200, content_type='text/html'):
        self.output = output
        self.content_type = content_type
        self.status = status
        self.headers = []
        
        if headers and isinstance(headers, list):
            self.headers = headers
    
    def add_header(self, key, value):
        self.headers.append((key, value))
    
    def send(self, start_response):
        status = "%d %s" % (self.status, HTTP_MAPPINGS.get(self.status))
        headers = [('Content-Type', "%s; charset=utf-8" % self.content_type)] + self.headers
        final_headers = []
        
        # Because Unicode is unsupported...
        for header in headers:
            final_headers.append((self.convert_to_ascii(header[0]), self.convert_to_ascii(header[1])))
        
        start_response(status, final_headers)
        
        if isinstance(self.output, unicode):
            return self.output.encode('utf-8')
        else:
            return self.output
    
    def convert_to_ascii(self, data):
        if isinstance(data, unicode):
            try:
                return data.encode('us-ascii')
            except UnicodeError, e:
                raise
        else:
            return str(data)


def handle_request(environ, start_response):
    """The main handler. Dispatches to the user's code."""
    try:
        request = Request(environ, start_response)
        
        (re_url, url, callback), kwargs = find_matching_url(request)
        response = callback(request, **kwargs)
    except Exception, e:
        return handle_error(e, request)
    
    if not isinstance(response, Response):
        response = Response(response)
    
    return response.send(start_response)


def handle_error(exception, request):
    """If an exception is thrown, deal with it and present an error page."""
    if not getattr(exception, 'hide_traceback', False):
        (e_type, e_value, e_tb) = sys.exc_info()
        message = "%s occurred on '%s': %s\nTraceback: %s" % (
            exception.__class__,
            request._environ['PATH_INFO'],
            exception,
            ''.join(traceback.format_exception(e_type, e_value, e_tb))
        )
        request._environ['wsgi.errors'].write(message)
    
    if isinstance(exception, RequestError):
        status = getattr(exception, 'status', 404)
    else:
        status = 500
    
    if status in ERROR_HANDLERS:
        return ERROR_HANDLERS[status](request, exception)
    
    return not_found(request, exception)


def find_matching_url(request):
    """Searches through the methods who've registed themselves with the HTTP decorators."""
    if not request.method in REQUEST_MAPPINGS:
        raise NotFound("The HTTP request method '%s' is not supported." % request.method)
    
    for url_set in REQUEST_MAPPINGS[request.method]:
        match = url_set[0].search(request.path)
        
        if match is not None:
            return (url_set, match.groupdict())
    
    raise NotFound("Sorry, nothing here.")


def add_slash(url):
    """Adds a trailing slash for consistency in urls."""
    if not url.endswith('/'):
        url = url + '/'
    return url


def content_type(filename):
    """
    Takes a guess at what the desired mime type might be for the requested file.
    
    Mostly only useful for static media files.
    """
    ct = 'text/plain'
    ct_guess = mimetypes.guess_type(filename)
    
    if ct_guess[0] is not None:
        ct = ct_guess[0]
    
    return ct


# Decorators

def get(url):
    """Registers a method as capable of processing GET requests."""
    def wrapped(method):
        def new(*args, **kwargs):
            return method(*args, **kwargs)
        # Register.
        re_url = re.compile("^%s$" % add_slash(url))
        REQUEST_MAPPINGS['GET'].append((re_url, url, new))
        return new
    return wrapped


def post(url):
    """Registers a method as capable of processing POST requests."""
    def wrapped(method):
        def new(*args, **kwargs):
            return method(*args, **kwargs)
        # Register.
        re_url = re.compile("^%s$" % add_slash(url))
        REQUEST_MAPPINGS['POST'].append((re_url, url, new))
        return new
    return wrapped


def put(url):
    """Registers a method as capable of processing PUT requests."""
    def wrapped(method):
        def new(*args, **kwargs):
            return method(*args, **kwargs)
        # Register.
        re_url = re.compile("^%s$" % add_slash(url))
        REQUEST_MAPPINGS['PUT'].append((re_url, url, new))
        new.status = 201
        return new
    return wrapped


def delete(url):
    """Registers a method as capable of processing DELETE requests."""
    def wrapped(method):
        def new(*args, **kwargs):
            return method(*args, **kwargs)
        # Register.
        re_url = re.compile("^%s$" % add_slash(url))
        REQUEST_MAPPINGS['DELETE'].append((re_url, url, new))
        return new
    return wrapped


def error(code):
    """Registers a method for processing errors of a certain HTTP code."""
    def wrapped(method):
        def new(*args, **kwargs):
            return method(*args, **kwargs)
        # Register.
        ERROR_HANDLERS[code] = new
        return new
    return wrapped


# Error handlers

@error(302)
def redirect(request, exception):
    response = Response('', status=302, content_type='text/plain', headers=[('Location', exception.url)])
    return response.send(request._start_response)

@error(403)
def forbidden(request, exception):
    response = Response('Forbidden', status=403, content_type='text/plain')
    return response.send(request._start_response)


@error(404)
def not_found(request, exception):
    response = Response('Not Found', status=404, content_type='text/plain')
    return response.send(request._start_response)


@error(500)
def app_error(request, exception):
    if (os.environ['SERVER_SOFTWARE'] == 'Development/1.0'):
      html_output = """
        <html><body><h1>Application Error!</h1><p>Exception: %s</p></body></html>
        """ % exception

      response = Response(html_output, status=500)
      return response.send(request._start_response)
    else:
      response = Response('Application Error', status=500, content_type='text/plain')
      return response.send(request._start_response)

# Server

def main(host='localhost', port=8080, config=None):

    if config is not None:
        # We'll let ImportErrors bubble up.
        config_options = __import__(config)
        host           = getattr(config_options, 'host'  , host)
        port           = getattr(config_options, 'port'  , port)
        server         = getattr(config_options, 'server', server)
    
    util.run_wsgi_app(handle_request)
    